/*
* Tiled Map Editor, (c) 2004-2006
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* Adam Turk <aturk@biggeruniverse.com> Bjorn Lindeijer <bjorn@lindeijer.nl>
*/
package de.yaams.extensions.basemap.tiled.mapeditor.widget;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
import javax.swing.JPanel;
import javax.swing.Scrollable;
import javax.swing.event.MouseInputAdapter;
import de.yaams.extensions.basemap.tiled.core.Tile;
import de.yaams.extensions.basemap.tiled.core.TileLayer;
import de.yaams.extensions.basemap.tiled.core.TileSet;
import de.yaams.extensions.basemap.tiled.core.TilesetChangeListener;
import de.yaams.extensions.basemap.tiled.core.TilesetChangedEvent;
import de.yaams.extensions.basemap.tiled.mapeditor.util.TileRegionSelectionEvent;
import de.yaams.extensions.basemap.tiled.mapeditor.util.TileSelectionEvent;
import de.yaams.extensions.basemap.tiled.mapeditor.util.TileSelectionListener;
/**
* Displays a tileset and allows selecting a specific tile as well as selecting
* several tiles for the creation of a stamp brush.
*/
public class TilePalettePanel extends JPanel implements Scrollable, TilesetChangeListener {
/**
*
*/
private static final long serialVersionUID = -8581958512078553539L;
private static final int TILES_PER_ROW = 4;
private TileSet tileset;
private List<TileSelectionListener> tileSelectionListeners;
private Vector<Tile> tilesetMap;
private Rectangle selection;
/**
* Constructs an empty tile palette panel.
*/
public TilePalettePanel() {
tileSelectionListeners = new LinkedList<TileSelectionListener>();
MouseInputAdapter mouseInputAdapter = new MouseInputAdapter() {
private Point origin;
@Override
public void mousePressed(MouseEvent e) {
origin = getTileCoordinates(e.getX(), e.getY());
setSelection(new Rectangle(origin.x, origin.y, 0, 0));
scrollTileToVisible(origin);
Tile clickedTile = getTileAt(origin.x, origin.y);
if (clickedTile != null) {
fireTileSelectionEvent(clickedTile);
}
}
@Override
public void mouseDragged(MouseEvent e) {
Point point = getTileCoordinates(e.getX(), e.getY());
Rectangle select = new Rectangle(origin.x, origin.y, 0, 0);
select.add(point);
if (!select.equals(selection)) {
setSelection(select);
scrollTileToVisible(point);
}
if (selection.getWidth() > 0 || selection.getHeight() > 0)
fireTileRegionSelectionEvent(selection);
}
};
addMouseListener(mouseInputAdapter);
addMouseMotionListener(mouseInputAdapter);
}
/**
* Adds tile selection listener. The listener will be notified when the user
* selects a tile.
*
* @param listener
* the listener to add
*/
public void addTileSelectionListener(TileSelectionListener listener) {
tileSelectionListeners.add(listener);
}
/**
* Removes tile selection listener.
*
* @param listener
* the listener to remove
*/
public void removeTileSelectionListener(TileSelectionListener listener) {
tileSelectionListeners.remove(listener);
}
private void fireTileSelectionEvent(Tile selectedTile) {
TileSelectionEvent event = new TileSelectionEvent(this, selectedTile);
for (TileSelectionListener listener : tileSelectionListeners) {
listener.tileSelected(event);
}
}
private void fireTileRegionSelectionEvent(Rectangle selection) {
TileLayer region = createTileLayerFromRegion(selection);
TileRegionSelectionEvent event = new TileRegionSelectionEvent(this, region);
for (TileSelectionListener listener : tileSelectionListeners) {
listener.tileRegionSelected(event);
}
}
/**
* Creates a tile layer from a certain region of the tile palette.
*
* @param rect
* the rectangular region from which a tile layer is created
* @return the created tile layer
*/
private TileLayer createTileLayerFromRegion(Rectangle rect) {
TileLayer layer = new TileLayer(rect.width + 1, rect.height + 1);
// Copy the tiles in the region to the tile layer
for (int y = rect.y; y <= rect.y + rect.height; y++) {
for (int x = rect.x; x <= rect.x + rect.width; x++) {
layer.setTileAt(x - rect.x, y - rect.y, getTileAt(x, y));
}
}
return layer;
}
/**
* Change the tileset displayed by this palette panel.
*
* @param tileset
* the tileset to be displayed by this palette panel
*/
public void setTileset(TileSet tileset) {
// Remove any existing listener
if (this.tileset != null) {
this.tileset.removeTilesetChangeListener(this);
}
this.tileset = tileset;
// Listen to changes in the new tileset
if (this.tileset != null) {
this.tileset.addTilesetChangeListener(this);
}
if (tileset != null)
tilesetMap = tileset.generateGaplessVector();
revalidate();
repaint();
}
public TileSet getTileset() {
return tileset;
}
@Override
public void tilesetChanged(TilesetChangedEvent event) {
tilesetMap = tileset.generateGaplessVector();
revalidate();
repaint();
}
@Override
public void nameChanged(TilesetChangedEvent event, String oldName, String newName) {}
@Override
public void sourceChanged(TilesetChangedEvent event, String oldSource, String newSource) {}
/**
* Converts pixel coordinates to tile coordinates. The returned coordinates
* are at least 0 and adjusted with respect to the number of tiles per row
* and the number of rows.
*
* @param x
* x coordinate
* @param y
* y coordinate
* @return tile coordinates
*/
private Point getTileCoordinates(int x, int y) {
int twidth = tileset.getTileWidth() + 1;
int theight = tileset.getTileHeight() + 1;
int tileCount = tilesetMap.size();
int tilesPerRow = getTilesPerRow();
int rows = tileCount / tilesPerRow + (tileCount % tilesPerRow > 0 ? 1 : 0);
int tileX = Math.max(0, Math.min(x / twidth, tilesPerRow - 1));
int tileY = Math.max(0, Math.min(y / theight, rows - 1));
return new Point(tileX, tileY);
}
/**
* Retrieves the tile at the given tile coordinates. It assumes the tile
* coordinates are adjusted to the number of tiles per row.
*
* @param x
* x tile coordinate
* @param y
* y tile coordinate
* @return the tile at the given tile coordinates, or <code>null</code> if
* the index is out of range
*/
private Tile getTileAt(int x, int y) {
int tilesPerRow = getTilesPerRow();
int tileAt = y * tilesPerRow + x;
if (tileAt >= tilesetMap.size()) {
return null;
} else {
return tilesetMap.get(tileAt);
}
}
/**
* Returns the number of tiles to display per row. This gets calculated
* dynamically unless the tileset specifies this value.
*
* @return the number of tiles to display per row, is at least 1
*/
private int getTilesPerRow() {
// todo: It should be an option to follow the tiles per row given
// todo: by the tileset.
if (tileset.getTilesPerRow() == 0) {
int twidth = tileset.getTileWidth() + 1;
return Math.max(1, (getWidth() - 1) / twidth);
} else {
return tileset.getTilesPerRow();
}
}
private void setSelection(Rectangle rect) {
repaintSelection();
selection = rect;
repaintSelection();
}
private void repaintSelection() {
if (selection != null) {
int twidth = tileset.getTileWidth() + 1;
int theight = tileset.getTileHeight() + 1;
repaint(selection.x * twidth, selection.y * theight, (selection.width + 1) * twidth + 1, (selection.height + 1) * theight + 1);
}
}
private void scrollTileToVisible(Point tile) {
int twidth = tileset.getTileWidth() + 1;
int theight = tileset.getTileHeight() + 1;
scrollRectToVisible(new Rectangle(tile.x * twidth, tile.y * theight, twidth + 1, theight + 1));
}
@Override
public void paint(Graphics g) {
Rectangle clip = g.getClipBounds();
paintBackground(g);
if (tileset != null) {
// Draw the tiles
int twidth = tileset.getTileWidth() + 1;
int theight = tileset.getTileHeight() + 1;
int tilesPerRow = getTilesPerRow();
int startY = clip.y / theight;
int endY = (clip.y + clip.height) / theight + 1;
int tileAt = tilesPerRow * startY;
int gx;
int gy = startY * theight;
for (int y = startY; y < endY; y++) {
gx = 1;
for (int x = 0; x < tilesPerRow && tileAt < tilesetMap.size(); x++, tileAt++) {
Tile tile = tilesetMap.get(tileAt);
if (tile != null) {
tile.drawRaw(g, gx, gy + theight, 1.0);
}
gx += twidth;
}
gy += theight;
}
// Draw the selection
if (selection != null) {
g.setColor(new Color(100, 100, 255));
g.draw3DRect(selection.x * twidth, selection.y * theight, (selection.width + 1) * twidth, (selection.height + 1) * theight,
false);
((Graphics2D) g).setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.2f));
g.fillRect(selection.x * twidth + 1, selection.y * theight + 1, (selection.width + 1) * twidth - 1, (selection.height + 1)
* theight - 1);
}
}
}
/**
* Draws checkerboard background.
*
* @param g
* the {@link Graphics} instance to draw on
*/
private static void paintBackground(Graphics g) {
Rectangle clip = g.getClipBounds();
int side = 10;
int startX = clip.x / side;
int startY = clip.y / side;
int endX = (clip.x + clip.width) / side + 1;
int endY = (clip.y + clip.height) / side + 1;
// Fill with white background
g.setColor(Color.WHITE);
g.fillRect(clip.x, clip.y, clip.width, clip.height);
// Draw darker squares
g.setColor(Color.LIGHT_GRAY);
for (int y = startY; y < endY; y++) {
for (int x = startX; x < endX; x++) {
if ((y + x) % 2 == 1) {
g.fillRect(x * side, y * side, side, side);
}
}
}
}
@Override
public Dimension getPreferredSize() {
if (tileset == null) {
return new Dimension(0, 0);
} else {
int twidth = tileset.getTileWidth() + 1;
int theight = tileset.getTileHeight() + 1;
int tileCount = tilesetMap.size();
int tilesPerRow = getTilesPerRow();
int rows = tileCount / tilesPerRow + (tileCount % tilesPerRow > 0 ? 1 : 0);
return new Dimension(tilesPerRow * twidth + 1, rows * theight + 1);
}
}
// Scrollable interface
@Override
public Dimension getPreferredScrollableViewportSize() {
if (tileset != null) {
int twidth = tileset.getTileWidth() + 1;
return new Dimension(TILES_PER_ROW * twidth + 1, 200);
} else {
return new Dimension(0, 0);
}
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
if (tileset != null) {
return tileset.getTileWidth();
} else {
return 0;
}
}
@Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
if (tileset != null) {
return tileset.getTileWidth();
} else {
return 0;
}
}
@Override
public boolean getScrollableTracksViewportWidth() {
// todo: Update when this has become an option
return tileset == null || tileset.getTilesPerRow() == 0;
}
@Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
}